/*!
\copyright  Copyright (c) 2023 - 2024 Qualcomm Technologies International, Ltd.
            All Rights Reserved.
            Qualcomm Technologies International, Ltd. Confidential and Proprietary.
\file
\brief      Main implementation of A2DP sync start.

*/

#include "sync_start_a2dp.h"
#include "sync_start_a2dp_audio.h"
#include "sync_start_a2dp_ctx.h"
#include "sync_start_a2dp_audio_sync.h"
#include "sync_start_a2dp_utils.h"
#include "sync_start_a2dp_debug_pio.h"

#include "aptx_ad_utils.h"

#include <output_mute.h>
#include <q2q_mode.h>

#include <kymera_internal_msg_ids.h>
#include <kymera_config.h>
#include <kymera_a2dp_private.h>
#include <kymera_data.h>
#ifdef INCLUDE_APTX_ADAPTIVE
#include <operators.h>
#include <chain.h>
#include <av_seids.h>
#endif

#include <logging.h>
#include <timestamp_event.h>
#include <message.h>
#include <panic.h>

static void syncStartA2dp_MessageHandler(Task task, MessageId id, Message message);
static void syncStartA2dp_StartGraphForMode(sync_start_a2dp_info_t *sync_info);

static const TaskData task_data = {.handler = syncStartA2dp_MessageHandler};

typedef struct {
    Source source;
} sync_start_a2dp_start_msg_t;

#define SYNC_START_A2DP_START (KYMERA_INTERNAL_A2DP_MESSAGE_MORE_DATA_TIMEOUT + 20)

typedef struct {
    Sink sink;
} sync_start_a2dp_start_mirroring_msg_t;

#define SYNC_START_A2DP_START_MIRRORING (SYNC_START_A2DP_START + 1)

Task SyncStartA2dp_GetTask(void)
{
    return (Task)&task_data;
}

void SyncStartA2dp_StartMirroring(Sink sync_sink)
{
    const uint16 *lock = SyncStartA2dp_GetA2dpNotPreparedLock();
    DEBUG_LOG("SyncStartA2dp_StartMirroring sink 0x%x", sync_sink);

    sync_start_a2dp_start_mirroring_msg_t *msg = PanicNull(malloc(sizeof(*msg)));
    memset(msg, 0, sizeof(*msg));
    msg->sink = sync_sink;

    if (lock)
    {
        MessageSendConditionally(SyncStartA2dp_GetTask(), SYNC_START_A2DP_START_MIRRORING, msg, lock);
    }
    else
    {
        MessageSend(SyncStartA2dp_GetTask(), SYNC_START_A2DP_START_MIRRORING, msg);
    }
}

static void syncStartA2dp_HandleStartMirroring(Message message)
{
    sync_start_a2dp_start_mirroring_msg_t *msg = (sync_start_a2dp_start_mirroring_msg_t *)message;

    sync_start_a2dp_ctx_t *ctx = SyncStartA2dp_GetCtx();
    mirror_profile_a2dp_start_mode_t mode = MirrorProfile_GetA2dpStartMode();
    DEBUG_LOG("syncStartA2dp_HandleStartMirroring a2dp_start_mode enum:mirror_profile_a2dp_start_mode_t:%d", mode);

    ctx->is_mirroring = TRUE;

    /* If already in sync unmute mode on primary, do not change decision */
    if (ctx->sync_info.mode != KYMERA_AUDIO_SYNC_START_PRIMARY_SYNC_UNMUTE)
    {
        /* audio sync mode must be refreshed in the event whereby secondary
         * device has joined pre-synchronised primary device */
        SyncStartA2dp_AudioSyncConfigureMode(&ctx->sync_info, mode);
    }


    /* the mirror link should now be up so we can configure the L2CAP filter */
    Sink media_sink = StreamSinkFromSource(ctx->media_source);
    if (ctx->aptx_ad_split_tx_mode)
    {
        AptxAdUtils_SetL2capFilter(media_sink);
    }
    else
    {
        AptxAdUtils_DisableL2capFilter(media_sink);
    }

    SyncStartA2dp_AudioSyncCreateAndConfigure(&ctx->sync_info, msg->sink, ctx->rtp_op);
}

void SyncStartA2dp_StopMirroring(Source source)
{
    MessageCancelAll(SyncStartA2dp_GetTask(), SYNC_START_A2DP_START_MIRRORING);

    /* Stop and destroy audio sync */
    sync_start_a2dp_ctx_t *ctx = SyncStartA2dp_GetCtx();

    /* Cancel message waiting to unmute secondary while we tear down A2DP Mirror link for A2DP sync task */
    MessageCancelAll(SyncStartA2dp_GetTask(), KYMERA_INTERNAL_A2DP_AUDIO_SYNCHRONISED);

    if (ctx->is_mirroring)
    {
        SyncStartA2dp_AudioSyncDestroy(SyncStartA2dp_GetCtx(), source, ctx->rtp_op);

        OutputMute_Set(FALSE);

        SyncStartA2dp_SyncCompleted();

        ctx->is_mirroring = FALSE;
    }
}

static void syncStartA2dp_HandleMessageMoreData(const MessageMoreData *mmd)
{
    sync_start_a2dp_ctx_t *ctx = SyncStartA2dp_GetCtx();
    sync_start_a2dp_info_t * info = SyncStartA2dp_GetInfo();

    if (mmd->source == ctx->media_source)
    {
        uint16 boundary = SourceBoundary(mmd->source);
        if (boundary)
        {
            SyncStartA2dp_PioSet();
            DEBUG_LOG("syncStartA2dp_HandleMessageMoreData boundary=%d", boundary);

#ifdef PRINT_RTP_HEADER
            const uint8 *ptr = SourceMap(mmd->source);
            DEBUG_LOG("**** %x %x %x %x ****", ptr[0], ptr[1], ptr[2], ptr[3]);
            DEBUG_LOG("**** %x %x %x %x ****", ptr[4], ptr[5], ptr[6], ptr[7]);
            DEBUG_LOG("**** %x %x %x %x ****", ptr[8], ptr[9], ptr[10], ptr[11]);
#endif

            SyncStartA2dp_MessageMoreDataCreateTransform(mmd->source);

            SyncStartA2dp_UnsubscribeFromMessageMoreData(mmd->source, SyncStartA2dp_GetTask());

            SyncStartA2dp_ScheduleDataSyncIndTimeoutMessage(info);

            if (Q2qMode_IsEnabled())
            {
                SyncStartA2dp_ScheduleQ2qUnmute(info->mode, appConfigSecondaryJoinsSynchronisedTrimMs() * US_PER_MS);
            }

            SyncStartA2dp_PioClr();
        }
    }
}

void SyncStartA2dp_ScheduleStartMessage(Source source)
{
    const uint16 *lock = SyncStartA2dp_GetA2dpNotPreparedLock();
    DEBUG_LOG_VERBOSE("SyncStartA2dp_ScheduleStartMessage source 0x%x", source);
    sync_start_a2dp_start_msg_t *msg = PanicNull(malloc(sizeof(*msg)));
    memset(msg, 0, sizeof(*msg));

    msg->source = source;

    if (lock)
    {
        MessageSendConditionally(SyncStartA2dp_GetTask(), SYNC_START_A2DP_START, msg, lock);
    }
    else
    {
        MessageSend(SyncStartA2dp_GetTask(), SYNC_START_A2DP_START, msg);
    }
}

bool SyncStartA2dp_CancelStartMessage(void)
{
    /* We expect only one START is ever queued at a time */
    uint16 cancelled = MessageCancelAll(SyncStartA2dp_GetTask(), SYNC_START_A2DP_START);
    DEBUG_LOG_INFO("SyncStartA2dp_CancelStartMessage: %d", cancelled);
    PanicFalse(cancelled <= 1);
    return cancelled != 0;
}

static void syncStartA2dp_HandleStartMessage(Message message)
{
    sync_start_a2dp_start_msg_t *msg = (sync_start_a2dp_start_msg_t *)message;
    SyncStartA2dp_StartGraphWhenInSync(msg->source);
}

void SyncStartA2dp_StartGraphWhenInSync(Source source)
{
    bool is_mirroring = TRUE;
#ifdef ENABLE_TWM_STEREO
    kymeraTaskData *theKymera = KymeraGetTaskData();
    is_mirroring = appKymera_IsStereoMirroring(theKymera);
#endif /* ENABLE_TWM_STEREO */
    DEBUG_LOG_VERBOSE("SyncStartA2dp_StartGraphWhenInSync source 0x%x", source);

    sync_start_a2dp_info_t *sync_info = SyncStartA2dp_GetInfo();
    SyncStartA2dp_GetCtx()->media_source = source;
    SyncStartA2dp_GetCtx()->is_streaming = TRUE;

    StreamDisconnect(source, 0);

    if(is_mirroring)
    {
        SyncStartA2dp_AudioSyncConfigureMode(sync_info, MirrorProfile_GetA2dpStartMode());
        /* The hash transform is created/connected when the first packet arrives from the
        source - signalled by a MESSAGE_MORE_DATA (MMD).
        In Q2Q mode, when audio sync required the transform for the packetiser created/started
        when the first packet arrives from the source - signalled by MESSAGE_MORE_DATA(MMD).
        Need to set timeout for MMD so if MMD is not received within time at the secondary,
        time to unmute message can be sent to primary to unmute the instant.
        Note: When audio sync required in Q2Q mode, KYMERA_AUDIO_SYNC_START_PRIMARY/SECONDARY_SYNC_UNMUTE
        are used. So having following check makes sure to set timeout for MMD only when no sync
        required in Q2Q mode. */
        if (!Q2qMode_IsEnabled())
        {
            MessageMoreData mmd = {source};

            /* No data in source, wait for MESSAGE_MORE_DATA */
            SyncStartA2dp_SubscribeForMessageMoreData(source, SyncStartA2dp_GetTask());

            /* Check if already data in the source */
            syncStartA2dp_HandleMessageMoreData(&mmd);
        }
        else
        {
            /* start/connect the pakcetiser transform as no audio sync required in Q2Q */
            SyncStartA2dp_MessageMoreDataCreateTransform(source);
        }
    }
    else
    {
        if (Q2qMode_IsEnabled())
        {
            SyncStartA2dp_MessageMoreDataCreateTransform(source);
        }
    }

    syncStartA2dp_StartGraphForMode(sync_info);
    SyncStartA2dp_PioClr(); /* Set before the chain is created */
}

/* Used to be appKymeraStartChains */
static void syncStartA2dp_StartGraphForMode(sync_start_a2dp_info_t *sync_info)
{
    bool start_input_chain_now = TRUE;
    bool is_mirroring = TRUE;
    kymeraTaskData *theKymera = KymeraGetTaskData();
#ifdef ENABLE_TWM_STEREO
    is_mirroring = appKymera_IsStereoMirroring(theKymera);
#endif /* ENABLE_TWM_STEREO */

    if (is_mirroring)
    {
        switch(sync_info->mode)
        {
        case KYMERA_AUDIO_SYNC_START_PRIMARY_UNSYNCHRONISED:
        case KYMERA_AUDIO_SYNC_START_Q2Q:
            /* Audio sync is not required in these modes. */
            /* No need to wait for Message More Data */
            SyncStartA2dp_GetCtx()->mmd_recived = TRUE;
            SyncStartA2dp_SyncCompleted();
        break;

        case KYMERA_AUDIO_SYNC_START_Q2Q_PRIMARY_UNMUTE:
        case KYMERA_AUDIO_SYNC_START_Q2Q_SECONDARY_UNMUTE:
        {
            if (sync_info->mode == KYMERA_AUDIO_SYNC_START_Q2Q_SECONDARY_UNMUTE)
            {
                SyncStartA2dp_ScheduleUnmute(VmGetTimerTime(), appConfigQ2QSynchronisedUnmuteDelayMs() * US_PER_MS);
            }
            else if (sync_info->mode == KYMERA_AUDIO_SYNC_START_Q2Q_PRIMARY_UNMUTE)
            {
                /* Timeout message in case secondary never triggers the unmute */
                SyncStartA2dp_ScheduleUnmuteTimeout();
            }
            OutputMute_Set(TRUE);
            DEBUG_LOG("syncStartA2dp_StartGraphForMode, mute");
            sync_info->state = KYMERA_AUDIO_SYNC_STATE_IN_PROGRESS;
        }
        break;

        case KYMERA_AUDIO_SYNC_START_PRIMARY_SYNC_UNMUTE:
        case KYMERA_AUDIO_SYNC_START_SECONDARY_SYNC_UNMUTE:
        {
            /* Mute audio output before starting the input chain to ensure that
             * audio chains consume audio data and play silence on the output
             * until the application receives sink synchronised indication.
             * The source sync gain is used instead of the volume control as this
             * gain only affects the input stream - the aux/main volume can be set
             * without affecting the source sync's mute of the input.
             */
            MessageCancelAll(SyncStartA2dp_GetTask(), KYMERA_INTERNAL_A2DP_AUDIO_SYNCHRONISED);
            MessageSendLater((Task)&task_data, KYMERA_INTERNAL_A2DP_AUDIO_SYNCHRONISED, NULL, A2DP_MIRROR_SYNC_UNMUTE_TIMEOUT_MS);
            OutputMute_Set(TRUE);
            DEBUG_LOG("syncStartA2dp_StartGraphForMode, mute");
            sync_info->state = KYMERA_AUDIO_SYNC_STATE_IN_PROGRESS;
        }
        break;
        default:
            /* delay start of input chain until the application receives
             * a2dp data sync indication.
             */
            start_input_chain_now = FALSE;
            sync_info->state = KYMERA_AUDIO_SYNC_STATE_INIT;

            uint32 sync_instant = 0;
            if (MirrorProfile_GetA2dpSyncInstant(&sync_instant))
            {
                DEBUG_LOG_INFO("syncStartA2dp_StartGraphForMode, using cached sync instant %x", sync_instant);
                SyncStartA2dp_SetStartTime(sync_instant);
            }
            else
            {
                DEBUG_LOG("syncStartA2dp_StartGraphForMode, input chain start delayed");
            }
        break;
        }
    }
    else
    {
        /* In Q2Q mode the media source has already been connected to the input
        chain by the TransformPacketise so the chain can be started immediately */
        if (!Q2qMode_IsEnabled())
        {
            /* The media source may fail to connect to the input chain if the source
            disconnects between the time A2DP asks Kymera to start and this
            function being called. A2DP will subsequently ask Kymera to stop. */
            start_input_chain_now = ChainConnectInput(theKymera->chain_input_handle, theKymera->media_source, EPR_SINK_MEDIA);
        }
    }
    SyncStartA2dp_StartGraph(!start_input_chain_now);
}

/* Called from MDM_L2CAP_DATA_SYNC_IND handler */
void SyncStartA2dp_SetStartTime(uint32 clock)
{
    sync_start_a2dp_info_t *sync_info = SyncStartA2dp_GetInfo();

    if (Q2qMode_IsEnabled())
    {
        return;
    }

    DEBUG_LOG("SyncStartA2dp_SetStartTime instant %x", clock);

    SyncStartA2dp_LogStates();

    /* Cancel any pending timeout messages */
    MessageCancelAll((Task)&task_data, KYMERA_INTERNAL_A2DP_DATA_SYNC_IND_TIMEOUT);

    if (SyncStartA2dp_IsA2dpStreaming() && ((sync_info->state == KYMERA_AUDIO_SYNC_STATE_INIT) || (sync_info->state == KYMERA_AUDIO_SYNC_STATE_IN_PROGRESS)))
    {
        SyncStartA2dp_PioSet();

        SyncStartA2dp_LogStartTime(clock);

        /* Configure the RTP operator in free_run mode until the application receives
            * synchronised indication from audio sync stream
            */
        DEBUG_LOG("SyncStartA2dp_SetStartTime, configure RTP operator in ttp_free_run mode");
        SyncStartA2dp_SetRtpTtpMode(ttp_free_run, &clock);

        if(sync_info->state == KYMERA_AUDIO_SYNC_STATE_INIT)
        {
        /* start the input chain */
        SyncStartA2dp_StartInputGraph();
        DEBUG_LOG("SyncStartA2dp_SetStartTime, started input chain");
        sync_info->state = KYMERA_AUDIO_SYNC_STATE_IN_PROGRESS;
		}
        SyncStartA2dp_PioClr();
    }
}

void SyncStartA2dp_SetSyncOperatorId(Operator sync_op_id)
{
    SyncStartA2dp_GetCtx()->rtp_op = sync_op_id;
}

static void syncStartA2dp_HandleAudioSyncStreamInd(Message msg)
{
    sync_start_a2dp_info_t *sync_info = SyncStartA2dp_GetInfo();

    if (!SyncStartA2dp_IsA2dpMirrored() || (sync_info->state == KYMERA_AUDIO_SYNC_STATE_COMPLETE))
    {
        return;
    }

    SyncStartA2dp_PioSet();

    switch(sync_info->mode)
    {
    case KYMERA_AUDIO_SYNC_START_PRIMARY_SYNCHRONISED:
        SyncStartA2dp_SyncCompleted();
        /* Now that audio sync source stream has been sychronised, switch
         * the audio RTP operator mode to normal (or TTP_FULL)
         */
        SyncStartA2dp_SetRtpTtpMode(ttp_full_only, NULL);
    break;

    case KYMERA_AUDIO_SYNC_START_SECONDARY_SYNCHRONISED:
        SyncStartA2dp_SyncCompleted();
    break;

    case KYMERA_AUDIO_SYNC_START_PRIMARY_SYNC_UNMUTE:
        SyncStartA2dp_SetRtpTtpMode(ttp_full_only, NULL);
        /* In case message from secondary doesn't arrive containing unmute instant
           send synchronised message after timeout to force unmute */
        SyncStartA2dp_ScheduleUnmuteTimeout();
        break;

    case KYMERA_AUDIO_SYNC_START_SECONDARY_SYNC_UNMUTE:
    {
        MessageSinkAudioSynchronised *audio_sync_msg = (MessageSinkAudioSynchronised *)msg;
        PanicNull(audio_sync_msg);
        SyncStartA2dp_ScheduleUnmute(audio_sync_msg->sync_time, appConfigSecondaryJoinsSynchronisedTrimMs() * US_PER_MS);

        SyncStartA2dp_SetRtpTtpMode(ttp_free_run_only, NULL);
    }
    break;
    default:
        /* Nothing to be done for other configurations */
    break;
    }
    SyncStartA2dp_PioClr();
}

void SyncStartA2dp_ForceSinkAudioSynchronized(void)
{
    syncStartA2dp_HandleAudioSyncStreamInd(NULL);
}

static void syncStartA2dp_HandleAudioSynchronisedInd(void)
{
    sync_start_a2dp_info_t *sync_info = SyncStartA2dp_GetInfo();

    DEBUG_LOG("syncStartA2dp_HandleAudioSynchronisedInd");

    /* Cancel any pending messages */
    MessageCancelAll(SyncStartA2dp_GetTask(), KYMERA_INTERNAL_A2DP_AUDIO_SYNCHRONISED);

    if (!SyncStartA2dp_IsA2dpMirrored() || (sync_info->state == KYMERA_AUDIO_SYNC_STATE_COMPLETE))
    {
        return;
    }

    switch(sync_info->mode)
    {
    case KYMERA_AUDIO_SYNC_START_SECONDARY_SYNC_UNMUTE:
    case KYMERA_AUDIO_SYNC_START_PRIMARY_SYNC_UNMUTE:
    case KYMERA_AUDIO_SYNC_START_Q2Q_PRIMARY_UNMUTE:
    case KYMERA_AUDIO_SYNC_START_Q2Q_SECONDARY_UNMUTE:
    {
        /* Now that A2DP audio is synchronised, unmute the source sync output */
        DEBUG_LOG("syncStartA2dp_HandleAudioSynchronisedInd, unmute. Q2Q mode %d", Q2qMode_IsEnabled());
        if (Q2qMode_IsEnabled())
        {
#ifdef INCLUDE_APTX_ADAPTIVE
            kymeraTaskData *theKymera = KymeraGetTaskData();
            if (theKymera->a2dp_seid == AV_SEID_APTX_ADAPTIVE_SNK)
            {
                Operator op = PanicZero(ChainGetOperatorByRole(theKymera->chain_input_handle, OPR_SWITCHED_PASSTHROUGH_CONSUMER));
                OperatorsSetSwitchedPassthruMode(op, spc_op_mode_passthrough);
            }
#endif
        }
        OutputMute_Set(FALSE);
        TimestampEvent(TIMESTAMP_EVENT_KYMERA_INTERNAL_A2DP_AUDIO_SYNCHRONISED);
        SyncStartA2dp_SyncCompleted();
    }
    break;
    default:
        Panic();
    break;
    }
}

/*! \brief Switch from a primary/secondary synchronised startup to a unsynchronised
           start on primary with secondary joining muted until synchronised.
*/
static void syncStartA2dp_SwitchToUnsyncStart(void)
{
    if (SyncStartA2dp_IsA2dpStreaming())
    {
        sync_start_a2dp_info_t *sync_info = SyncStartA2dp_GetInfo();

        switch(sync_info->mode)
        {
            case KYMERA_AUDIO_SYNC_START_PRIMARY_SYNCHRONISED:
                sync_info->mode = KYMERA_AUDIO_SYNC_START_PRIMARY_SYNC_UNMUTE;
                /* In case message from secondary doesn't arrive containing unmute instant
                send synchronised message after timeout to force unmute */
                SyncStartA2dp_ScheduleUnmuteTimeout();
            break;
            case KYMERA_AUDIO_SYNC_START_SECONDARY_SYNCHRONISED:
                sync_info->mode = KYMERA_AUDIO_SYNC_START_SECONDARY_SYNC_UNMUTE;
            break;
            default:
            return;
        }
        syncStartA2dp_StartGraphForMode(sync_info);
    }
}

static void syncStartA2dp_MessageHandler(Task task, MessageId id, Message message)
{
    UNUSED(task);

    switch(id)
    {
        case MESSAGE_MORE_DATA:
            syncStartA2dp_HandleMessageMoreData((const MessageMoreData *)message);
        break;

        case MESSAGE_SINK_AUDIO_SYNCHRONISED:
        case MESSAGE_SOURCE_AUDIO_SYNCHRONISED:
            syncStartA2dp_HandleAudioSyncStreamInd(message);
        break;

        case KYMERA_INTERNAL_A2DP_AUDIO_SYNCHRONISED:
            syncStartA2dp_HandleAudioSynchronisedInd();
        break;

        case KYMERA_INTERNAL_A2DP_DATA_SYNC_IND_TIMEOUT:
            DEBUG_LOG_ERROR("KYMERA_INTERNAL_A2DP_DATA_SYNC_IND_TIMEOUT current state enum:sync_start_a2dp_stat_t:%d", SyncStartA2dp_GetInfo()->state);
            if(SyncStartA2dp_GetInfo()->state != KYMERA_AUDIO_SYNC_STATE_COMPLETE)
            {
                syncStartA2dp_SwitchToUnsyncStart();
            }
        break;

        case KYMERA_INTERNAL_A2DP_MESSAGE_MORE_DATA_TIMEOUT:
            DEBUG_LOG_ERROR("KYMERA_INTERNAL_A2DP_MESSAGE_MORE_DATA_TIMEOUT current state enum:sync_start_a2dp_stat_t:%d", SyncStartA2dp_GetInfo()->state);
            if(SyncStartA2dp_GetInfo()->state != KYMERA_AUDIO_SYNC_STATE_COMPLETE)
            {
                SyncStartA2dp_GetCtx()->mmd_recived = TRUE;
                syncStartA2dp_SwitchToUnsyncStart();
            }
        break;

        case SYNC_START_A2DP_START:
            DEBUG_LOG_ERROR("SYNC_START_A2DP_START");
            syncStartA2dp_HandleStartMessage(message);
        break;

        case SYNC_START_A2DP_START_MIRRORING:
            DEBUG_LOG_ERROR("SYNC_START_A2DP_START_MIRRORING");
            syncStartA2dp_HandleStartMirroring(message);
        break;

        default:
            DEBUG_LOG_ERROR("syncStartA2dp_MessageHandler unexpected message");
    }
}

void SyncStartA2dp_StartAudioSync(bool is_primary, uint32_t latency_in_us)
{
    SyncStartA2dp_AudioSyncStart(is_primary, latency_in_us);
}

void SyncStartA2dp_PauseAudioSync(void)
{
    SyncStartA2dp_AudioSyncPause();
}

void SyncStartA2dp_ResumeAudioSync(void)
{
    SyncStartA2dp_AudioSyncResume();
}

bool SyncStartA2dp_IsAudioSyncComplete(void)
{
    sync_start_a2dp_info_t *a2dp_sync_info = SyncStartA2dp_GetInfo();
    return (a2dp_sync_info->state == KYMERA_AUDIO_SYNC_STATE_COMPLETE);
}
